package de.pyranja.collections;

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import com.google.common.base.Ticker;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;

/**
 * <p>A cache based collection, that evicts entries after a set timeout is exceeded.
 * Essentially a thin wrapper around a guava cache, mimicking a collection.
 * The iterator returned by this collection is not able to modify the contents 
 * of this collection, but may reflect the current state of the collection
 * (weak consistency).</p> 
 * 
 * <p>All modifying methods will return true, indicating a 
 * structural change on the collection, even if no change happened. Do not rely 
 * on these return values!<br />
 * Note that unlike classic collections, <code>size()</code> and <code>isEmpty()
 * </code> are not constant operations and furthermore do not provide consistent 
 * results, but at best an estimation.</p>
 * 
 * <p>In addition to the methods defined in the Collection Interface, there are 
 * retrieval operations that extract elements based on the time of addition as 
 * provided by the set <code>Ticker</code>.</p>
 * 
 * @author Chris Borckholder
 * @param <T> type of stored objects
 */
public class ExpiringCollection<T> extends AbstractCollection<T> {
	
	// default config
	private static final int CONCURRENCY_LEVEL = 4;	// == guava default
	private static final long DEF_TIMEOUT = TimeUnit.MINUTES.toNanos(5L);

	// holds the stored values
	private final Cache<T, Long> data;
	// timing source
	private final Ticker timer;
	
	/**
	 * ExpiringCollection with default timeout 
	 */
	public ExpiringCollection() {
		this(DEF_TIMEOUT,TimeUnit.NANOSECONDS);
	}
	
	/**
	 * Offers only default options. Equivalent to
	 * <p><code>ExpiringCollection temp = new ExpiringCollection();<br />
	 * temp.addAll(collection);</code></p>
	 * @param c
	 */
	public ExpiringCollection(Collection<? extends T> c) {
		this();
		this.addAll(c);
	}
	
	/**
	 * ExpiringCollection with a custom timeout
	 * @param timeout
	 * @param unit
	 */
	public ExpiringCollection(final long timeout, final TimeUnit unit) {
		this(timeout,unit,Ticker.systemTicker());
	}
	
	/**
	 * ExpiringCollection with custom timeout and time source
	 * @param timeout
	 * @param unit
	 * @param time
	 */
	public ExpiringCollection(final long timeout, final TimeUnit unit, 
			final Ticker timer) {
		this.timer = timer;
		data = CacheBuilder.newBuilder()
					.concurrencyLevel(CONCURRENCY_LEVEL)
					.expireAfterWrite(timeout, unit)
					.ticker(timer)
					.build();
	}

	/**
	 * Computes an estimate of the current size of this collection, that may 
	 * include already expired elements. If the estimation exceeds 
	 * <code>Integer.MAX_VALUE</code>, <code>Integer.MAX_VALUE</code> is returned.
	 */
	@Override
	public int size() {
		final long estimate = data.size();
		if (estimate > Integer.MAX_VALUE) {
			return Integer.MAX_VALUE;
		}
		return (int) estimate;
	}
	
	/**
	 * Returns true if this collection contains the specified element. More 
	 * formally, returns true if and only if this collection contains at least 
	 * one element e such that<br /> 
	 * <code>(o==null ? e==null : o.equals(e))</code><br /> 
	 */
	@Override
	public boolean contains(final Object o) {
		return data.getIfPresent(o) == null ? false : true;
	}

	/**
	 * {@inheritDoc}
	 * The iterator is not able to modify the underlying collection.
	 */
	@Override
	public Iterator<T> iterator() {
		return Collections.unmodifiableSet(data.asMap().keySet()).iterator();
	}
	
	// RETRIEVAL with time based filtering
	// All this methods return immutable snapshots of the cache
	/**
	 * Returns all elements in this collection that were added <b>before</b> or 
	 * at the specified point in time. Note that the recorded creation time of 
	 * an element depends on the set Ticker, does not necessarily reflect real
	 * world time. You should use previously acquired reference time points,
	 * retrieved from {@link #reference()}.
	 * @param time reference time point
	 * @return an immutable snapshot of elements that match the criteria
	 */
	public Collection<T> before(long time) {
		final Set<Entry<T,Long>> entries = data.asMap().entrySet();
		final ImmutableList.Builder<T> out = ImmutableList.builder();
		for (Entry<T,Long> curr : entries) {
			if (curr.getValue() <= time) {
				out.add(curr.getKey());
			}
		}
		return out.build();
	}
	
	/**
	 * Returns all elements in this collection that were added <b>after</b> or 
	 * at the specified point in time. Note that the recorded creation time of
	 * an element depends on the set Ticker, does not necessarily reflect real
	 * world time. You should use previously acquired reference time points,
	 * retrieved from {@link #reference()}. 
	 * @param time reference time point
	 * @return an immutable snapshot of elements that match the criteria
	 */
	public Collection<T> after(long time) {
		final Set<Entry<T,Long>> entries = data.asMap().entrySet();
		final ImmutableList.Builder<T> out = ImmutableList.builder();
		for (Entry<T,Long> curr : entries) {
			if (curr.getValue() >= time) {
				out.add(curr.getKey());
			}
		}
		return out.build();
	}
	
	/**
	 * Returns a reference value marking a point in time that this collection
	 * regards as <i>now</i>. Use these references to filter elements through
	 * {@link #before()} or {@link #after()}.
	 * @return a long value marking a point in time
	 */
	public long reference() {
		return timer.read();
	}
	
	/**
	 * Forces a collection of expired elements
	 */
	public void clean() {
		data.cleanUp();
	}
	
	// MODIFYING METHODS
	// Add will associate with dummy value
	/**
	 * Ensures that an element is part of this collection. The time of addition
	 * as provided by the set Ticker is recorded and the element is from now on
	 * susceptible for expiration.<br />
	 * This implementation will <b>always</b> return true, indicating a structural
	 * change, even if no change occurred.
	 */
	@Override
	public boolean add(final T e) {
		data.put(e, timer.read());
		return true;
	}
	
	/**
	 * Adds all elements contained in the passed collection. The addition of the
	 * collection is not atomically, but all elements added this way will have
	 * the same addition time recorded.
	 */
	@Override
	public boolean addAll(final Collection<? extends T> c) {
		// read time once
		final long now = timer.read();
		// simple implementation identical to data.putAll(), according to doc  
		for (T curr : c) {
			data.put(curr, now);
		}
		return true;
	}
	
	/**
	 * The specified element will eventually be removed from this collection.
	 */
	@Override
	public boolean remove(final Object e) {
		data.invalidate(e);
		return true;
	}
	
	/**
	 * All elements contained on the specified collection will eventually be 
	 * removed from this collection.
	 */
	@Override
	public boolean removeAll(final Collection<?> c) {
		data.invalidateAll(c);
		return true;
	}

	/**
	 * Removes all elements from this collection.
	 */
	@Override
	public void clear() {
		data.invalidateAll();
	}
}